fix: null-guard pending_task in sync_start drain election (sim segfault) - #898
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughBoth scheduler implementations add a concurrency guard in the drain completion path. When an elected drain thread detects that ChangesDrain Protocol Concurrency Guard
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a null check for slot_state (representing drain_state_.pending_task) in scheduler_completion.cpp for both the a2a3 and a5 runtimes to handle concurrent drain completions. However, the review comments identify a severe concurrency race and state corruption risk: unconditionally clearing drain_ack_mask and sync_start_pending when slot_state is null can overwrite and destroy the active state of a subsequent, concurrently started drain run, potentially leading to permanent hangs. The reviewer suggests only resetting drain_worker_elected to release the stale election lock, leaving the other state variables untouched.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp`:
- Around line 423-433: The guard race is due to
SyncStartDrainState::pending_task being a non-atomic pointer; make pending_task
an std::atomic<PTO2TaskSlotState*> and use explicit release/acquire semantics:
in drain_worker_dispatch() replace the non-atomic clear with
pending_task.store(nullptr, std::memory_order_release) (and any place that sets
it use store(..., std::memory_order_release)), and in the elected-path (the
CAS-success branch that reads pending_task around the null-check and
dereference) perform an acquire load
(pending_task.load(std::memory_order_acquire)) before checking for nullptr and
before dereferencing; alternatively, if you prefer fences, insert a
std::atomic_thread_fence(std::memory_order_acquire) in the elected read path and
a release fence in the clearing path to establish the happens-before with
drain_worker_elected/sync_start_pending updates (symbols: SyncStartDrainState,
pending_task, drain_worker_dispatch, drain_worker_elected, sync_start_pending).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2c6caae0-6328-44a4-9b96-16d3ea6ddfef
📒 Files selected for processing (2)
src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cppsrc/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
…im segfault) SchedulerContext::handle_drain_mode() dereferences drain_state_.pending_task on the elected-worker path without a null check; under a workload that issues several sync_start tasks back-to-back with normal tasks holding clusters (spmd_sync_start_stress) an elected thread can observe pending_task == nullptr and crash. The sibling drain_worker_dispatch() already guards this; the elected path did not. This is the residual a2a3sim/a5sim SIGSEGV (rc=-11) under CPU oversubscription in CI (cf. hw-native-sys#884), distinct from the init-handshake hang fixed in hw-native-sys#893. A core dump pinned the fault to handle_drain_mode + ActiveMask::to_shape with this=0x30 (null slot_state). Fix (a2a3 + a5, identical code): 1. Promote pending_task to std::atomic<PTO2TaskSlotState *> with release-store on set/clear and acquire-load on the elected/dispatch reads. The pointer was plain memory shared across scheduler threads, cleared with relaxed ordering before the gate reopened, so a reader could also observe a stale non-null value and deref a recycled slot. The acquire/release pairing closes that. 2. Null-guard the elected path: if pending_task is null, the drain already completed and this is a stale-elected thread -- release only drain_worker_elected and return. It must NOT clear drain_ack_mask / sync_start_pending, which could belong to a concurrently-started drain run and whose loss would hang it. Verified in a --cpus=2 container: spmd_sync_start_stress at 8x concurrency crashed ~1/16 runs before; after the fix, 0 segfaults across 300+ runs. Onboard unaffected (missing-guard + ordering bug, not platform-specific). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
afd0f25 to
576723b
Compare
…im segfault) (hw-native-sys#898) SchedulerContext::handle_drain_mode() dereferences drain_state_.pending_task on the elected-worker path without a null check; under a workload that issues several sync_start tasks back-to-back with normal tasks holding clusters (spmd_sync_start_stress) an elected thread can observe pending_task == nullptr and crash. The sibling drain_worker_dispatch() already guards this; the elected path did not. This is the residual a2a3sim/a5sim SIGSEGV (rc=-11) under CPU oversubscription in CI (cf. hw-native-sys#884), distinct from the init-handshake hang fixed in hw-native-sys#893. A core dump pinned the fault to handle_drain_mode + ActiveMask::to_shape with this=0x30 (null slot_state). Fix (a2a3 + a5, identical code): 1. Promote pending_task to std::atomic<PTO2TaskSlotState *> with release-store on set/clear and acquire-load on the elected/dispatch reads. The pointer was plain memory shared across scheduler threads, cleared with relaxed ordering before the gate reopened, so a reader could also observe a stale non-null value and deref a recycled slot. The acquire/release pairing closes that. 2. Null-guard the elected path: if pending_task is null, the drain already completed and this is a stale-elected thread -- release only drain_worker_elected and return. It must NOT clear drain_ack_mask / sync_start_pending, which could belong to a concurrently-started drain run and whose loss would hang it. Verified in a --cpus=2 container: spmd_sync_start_stress at 8x concurrency crashed ~1/16 runs before; after the fix, 0 segfaults across 300+ runs. Onboard unaffected (missing-guard + ordering bug, not platform-specific).
Summary
Fixes the residual a2a3sim/a5sim
SIGSEGV(rc=-11) seen under CPU oversubscription in CI (cf. #884) — distinct from the init-handshake hang fixed in #893.SchedulerContext::handle_drain_mode()dereferencesdrain_state_.pending_taskon the elected-worker path without a null check:PTO2TaskSlotState *slot_state = drain_state_.pending_task; PTO2ResourceShape shape = slot_state->active_mask.to_shape(); // <-- deref, crashespending_taskis plain (non-atomic) memory, transientlynullptrbetweendrain_worker_dispatch()nulling it and the gate (sync_start_pending) re-opening. A workload that fires severalsync_starttasks back-to-back with normal tasks holding clusters (spmd_sync_start_stress) lets an elected thread observepending_task == nullptr→ crash. The siblingdrain_worker_dispatch()already guards this exact case;handle_drain_mode()didn't.Root cause evidence
A core dump pinned the fault precisely:
this=0x30=&((PTO2TaskSlotState*)0)->active_mask→slot_statewasnullptr.Fix
Add the null guard on the elected path (a2a3 + a5, identical code): if
pending_taskis null, open the gate (reset ack/elected, clearsync_start_pending) and return — mirroringdrain_worker_dispatch().Test plan
docker --cpus=2container (worse than CI's ~4 vCPU):spmd_sync_start_stressat 8× concurrency crashed ~1 in 16 runs before the fix.st-sim-a2a3/st-sim-a5under load.SPIN_WAIT_HINT-style no-op concerns don't apply).Refs #884, #893.